﻿using System;
#if !WINDOWS_PHONE
using System.Collections.Concurrent;
#endif
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO.IsolatedStorage;
using System.Linq;
#if NETFX_CORE
using System.Reflection;
#endif
using System.Resources;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using DaveSexton.Labs.Properties;

namespace DaveSexton.Labs
{
	/// <summary>
	/// Provides an entry point for running experiments in a controlled environment, for performing
	/// public integration and regression testing, and for encapsulating working examples that can
	/// serve as executable documentation.
	/// </summary>
	/// <remarks>
	/// <include file='Lab.xml' path='type[@id="Lab"]/remarks[@kind="intro"]/*'/>
	/// <include file='Lab.xml' path='property[@id="DisplayName"]/*'/>
	/// <include file='Lab.xml' path='property[@id="Description"]/*'/>
	/// <include file='Lab.xml' path='property[@id="Categories"]/*'/>
	/// <include file='Lab.xml' path='type[@id="Lab"]/remarks[@kind="remainder"]/*'/>
	/// </remarks>
	[ContractClass(typeof(LabContract))]
	public abstract class Lab : LabTraceSource, ILab
	{
		#region Public Properties
		/// <summary>
		/// Gets the name of the <see cref="Lab"/>.
		/// </summary>
		/// <remarks>
		/// <include file='Lab.xml' path='property[@id="DisplayName"]/*'/>
		/// </remarks>
		public string DisplayName
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
#endif

				return displayName;
			}
		}

		/// <summary>
		/// Gets a <see cref="string"/> containing the <see cref="LabCategory.OrganizedName"/> and the lab's <see cref="DisplayName"/>, 
		/// or just the <see cref="DisplayName"/> if the lab has not been processed by <see cref="LabCategory.Organize"/>.
		/// </summary>
		/// <remarks>
		/// Unlike the <see cref="FullCategory"/> property, <see cref="OrganizedName"/> excludes all parent categories that are shared 
		/// by all of the labs.  For example, if you have the following three labs: 
		/// <list type="bullet">
		/// <item>MyProject.TestLabs.Lab1</item>
		/// <item>MyProject.TestLabs.Fun.Lab2</item>
		/// <item>MyProject.TestLabs.Fun.Lab3</item>
		/// <item>MyProject.TestLabs.Boring.Lab4</item>
		/// </list>
		/// then the <see cref="OrganizedName"/> of each will be, respectively: 
		/// <list type="bullet">
		/// <item>Lab1</item>
		/// <item>Fun / Lab2</item>
		/// <item>Fun / Lab3</item>
		/// <item>Boring / Lab4</item>
		/// </list>
		/// </remarks>
		public string OrganizedName
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
#endif

				string value = displayName;

				LabCategory category = Category;

				if (category != null && !category.IsRoot && category != LabCategory.Empty)
				{
					value = string.Format(CultureInfo.CurrentCulture, Resources.CategoryNameFormat, category.OrganizedName, value);

#if !WINDOWS_PHONE_70
					Contract.Assume(!string.IsNullOrWhiteSpace(value));
#else
					Contract.Assume(!string.IsNullOrEmpty(value));
#endif
				}

				return value;
			}
		}

		/// <summary>
		/// Gets a <see cref="string"/> containing the <see cref="FullCategory"/> and <see cref="DisplayName"/> of the lab.
		/// </summary>
		public string FullName
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
#endif

				var value = string.Format(CultureInfo.CurrentCulture, Resources.CategoryNameFormat, FullCategory, displayName);

#if !WINDOWS_PHONE_70
				Contract.Assume(!string.IsNullOrWhiteSpace(value));
#else
				Contract.Assume(!string.IsNullOrEmpty(value));
#endif

				return value;
			}
		}

		/// <summary>
		/// Gets a brief textual description of the <see cref="Lab"/>.
		/// </summary>
		/// <remarks>
		/// <include file='Lab.xml' path='property[@id="Description"]/*'/>
		/// </remarks>
		public string Description
		{
			get
			{
				Contract.Ensures(Contract.Result<string>() != null);

				return description;
			}
		}

		/// <summary>
		/// Gets the <see cref="LabCategory"/> associated by <see cref="LabCategory.Organize"/>, or an empty category if 
		/// this lab has not been processed by <see cref="LabCategory.Organize"/>.
		/// </summary>
		public LabCategory Category
		{
			get;
			internal set;
		}

		/// <summary>
		/// Gets the categories in which this <see cref="Lab"/> is defined.
		/// </summary>
		/// <remarks>
		/// <include file='Lab.xml' path='property[@id="Categories"]/*'/>
		/// </remarks>
		public IList<string> Categories
		{
			get
			{
				Contract.Ensures(Contract.Result<IList<string>>() != null);
				Contract.Ensures(Contract.Result<IList<string>>().IsReadOnly);

				return categories;
			}
		}

		/// <summary>
		/// Gets a concatenated list of categories in which this <see cref="Lab"/> is defined.
		/// </summary>
		/// <remarks>
		/// <include file='Lab.xml' path='property[@id="Categories"]/*'/>
		/// </remarks>
		public string FullCategory
		{
			get
			{
				Contract.Ensures(Contract.Result<string>() != null);

				var value = categories.Aggregate(
						new StringBuilder(),
						(builder, category) => builder.Append(' ').Append(category),
						builder => builder.Length > 0 ? builder.ToString(1, builder.Length - 1) : builder.ToString());

				Contract.Assume(value != null);

				return value;
			}
		}

		/// <summary>
		/// Gets whether the <see cref="Lab"/> may be cancelled.
		/// </summary>
		/* TOOD: Create a Cancel() method that allows labs to be cancelled asynchronously 
		 * from a GUI (such as a WPF host) when this property is true. */
		public bool IsCancellable
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets whether the lab should be included when it's discovered by a <see cref="LabController"/>.
		/// </summary>
		public bool IsEnabled
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets whether the <see cref="LabController"/> should pause for user 
		/// input after the <see cref="Lab"/> completes, but only when it's not the final 
		/// <see cref="Lab"/> to be executed.  The default value is <see langword="false" />.
		/// </summary>
		public bool PauseOnCompletion
		{
			get;
			set;
		}

		/// <summary>
		/// This property is not intended to be used directly on a <see cref="Lab"/> instance.
		/// </summary>
		bool ILab.HasProxy
		{
			get
			{
				Contract.Ensures(!Contract.Result<bool>());

				return false;
			}
		}

		/// <summary>
		/// This property is not intended to be used directly on a <see cref="Lab"/> instance.
		/// </summary>
		Lab ILab.Proxy
		{
			get
			{
				Contract.Ensures(Contract.Result<Lab>() == null);

				return null;
			}
			set
			{
				throw new NotSupportedException();
			}
		}

		/// <summary>
		/// Gets whether the lab is a proxy for <see cref="ActualLab"/>.
		/// </summary>
		public bool IsProxy
		{
			get
			{
				return actualLab != this;
			}
		}

		/// <summary>
		/// Gets the actual <see cref="ILab"/> instance that this <see cref="Lab"/> represents.
		/// </summary>
		/// <remarks>
		/// The <see cref="ActualLab"/> property is useful for UI labs that derive from a control base
		/// and implement <see cref="ILab"/>.  Since the <see cref="Lab"/> object doesn't derive from 
		/// the control base, the UI lab host must be able to retrieve a reference to the underlying 
		/// <see cref="ILab"/> instance.
		/// </remarks>
		public ILab ActualLab
		{
			get
			{
				Contract.Ensures(Contract.Result<ILab>() == actualLab);

				return actualLab;
			}
		}

		/// <summary>
		/// Gets the lab's source code.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The <see cref="SourceCode"/> property depends upon lab applications using the <strong>Labs.targets</strong>
		/// file as part of its normal build process.  This file embeds the source code for all types that implement
		/// <see cref="ILab"/> into the target assembly.
		/// </para>
		/// <para>
		/// The <see cref="SourceCode"/> property is virtual so that derived types may acquire the lab's source code 
		/// using a different mechanism.
		/// </para>
		/// </remarks>
		/// <value>The lab's source code if found; otherwise, <see langword="null"/>.</value>
		public virtual string SourceCode
		{
			[DebuggerStepThrough]
			get
			{
				if (!sourceCodeLoaded)
				{
					var resources = SourceCodeResourceManager;

					if (resources != null)
					{
						try
						{
							sourceCode = resources.GetString(actualLab.GetType().FullName, System.Globalization.CultureInfo.CurrentCulture);
						}
						catch (MissingManifestResourceException)
						{
						}

						if (!string.IsNullOrEmpty(sourceCode))
						{
							sourceCode = FormatSourceCode(sourceCode);
						}
					}

					sourceCodeLoaded = true;
				}

				return sourceCode;
			}
		}

		/// <summary>
		/// Gets the lab's source XAML.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The <see cref="SourceXaml"/> property depends upon lab applications using the <strong>Labs.targets</strong>
		/// file as part of its normal build process.  This file embeds the source XAML for all types that implement 
		/// <see cref="ILab"/> into the target assembly.
		/// </para>
		/// <para>
		/// The <see cref="SourceXaml"/> property is virtual so that derived types may acquire the lab's source XAML 
		/// using a different mechanism.
		/// </para>
		/// </remarks>
		/// <value>The lab's source XAML if found; otherwise, <see langword="null"/>.</value>
		public virtual string SourceXaml
		{
			[DebuggerStepThrough]
			get
			{
				if (!sourceXamlLoaded)
				{
					var resources = SourceXamlResourceManager;

					if (resources != null)
					{
						try
						{
							sourceXaml = resources.GetString(actualLab.GetType().FullName, System.Globalization.CultureInfo.CurrentCulture);
						}
						catch (MissingManifestResourceException)
						{
						}

						if (!string.IsNullOrEmpty(sourceXaml))
						{
							sourceXaml = FormatSourceXaml(sourceXaml);
						}
					}

					sourceXamlLoaded = true;
				}

				return sourceXaml;
			}
		}
		#endregion

		#region Private / Protected
		protected internal ResourceManager SourceCodeResourceManager
		{
			get;
			set;
		}

		protected internal ResourceManager SourceXamlResourceManager
		{
			get;
			set;
		}

		protected TraceListener Listener
		{
			get
			{
				Contract.Ensures(Contract.Result<TraceListener>() != null);

				var value = listener.Value;

				Contract.Assume(value != null);

				return value;
			}
		}

		protected internal bool IsRunningAllExperiments
		{
			get;
			private set;
		}

		protected internal virtual IEnumerable<IExperiment> Experiments
		{
			get
			{
				Contract.Ensures(Contract.Result<IEnumerable<IExperiment>>() != null);

				var value = virtualExperiments.Value;

				Contract.Assume(value != null);

				return value;
			}
		}

		private IEnumerable<IExperiment> OverriddenExperiments
		{
			get
			{
				Contract.Ensures(Contract.Result<IEnumerable<IExperiment>>() != null);

				return overriddenExperiments == null ? Experiments : overriddenExperiments;
			}
		}

		protected internal static IsolatedStorageFile Storage
		{
			get
			{
				Contract.Ensures(Contract.Result<IsolatedStorageFile>() != null);

				var value = storage.Value;

				Contract.Assume(value != null);

				return value;
			}
		}

		protected internal string StorageDirectory
		{
			get
			{
#if !WINDOWS_PHONE_70
				Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
#else
				Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
#endif

				if (storageDirectory == null)
				{
					var path = categories.Aggregate(
							new StringBuilder(),
							(builder, category) => builder.Append(category).Append('\\'),
							builder => builder.Append(DisplayName).Append('\\').ToString());

					Contract.Assume(path != null);

					foreach (var c in System.IO.Path.GetInvalidPathChars())
					{
						path = path.Replace(c, '_');
					}

					storageDirectory = path;
				}

				if (!Storage.DirectoryExists(storageDirectory))
				{
					Storage.CreateDirectory(storageDirectory);
				}

#if !WINDOWS_PHONE_70
				Contract.Assume(!string.IsNullOrWhiteSpace(storageDirectory));
#else
				Contract.Assume(!string.IsNullOrEmpty(storageDirectory));
#endif

				return storageDirectory;
			}
		}

		internal const string StartButtonTextDefault = "Start Lab";
		internal const string StartButtonTextExperiment = "Start Experiment";
		private const string defaultLabName = "Lab";
		private const string defaultTraceSourceName = defaultLabName;
		private static readonly string[] suffixes = new[] { "lab", "labs" };
#if !WINDOWS_PHONE
		private static readonly Lazy<IsolatedStorageFile> storage = new Lazy<IsolatedStorageFile>(CreateStorage, isThreadSafe: true);
#else
		private static readonly Lazy<IsolatedStorageFile> storage = new Lazy<IsolatedStorageFile>(CreateStorage);
#endif

		private readonly AsyncTaskQueue registeredTasks = new AsyncTaskQueue();
		private readonly Lazy<IList<IExperiment>> virtualExperiments;
		private readonly Lazy<TraceListener> listener;
		private readonly string displayName, description;
		private readonly IList<string> categories;
		private readonly ILab actualLab;
		private IList<IExperiment> overriddenExperiments;
		private bool sourceCodeLoaded, sourceXamlLoaded;
		private string sourceCode, sourceXaml;
		private string storageDirectory;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of <see cref="Lab"/> for derived classes.
		/// </summary>
		protected Lab()
			: this(enabled: true)
		{
			Contract.Ensures(!IsProxy);
		}

		/// <summary>
		/// Constructs a new instance of <see cref="Lab"/> for derived classes.
		/// </summary>
		/// <param name="enabled">Indicates whether the lab is enabled by default and assigns <see cref="IsEnabled"/>.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors",
				Justification = "Code Contracts rewriter")]
		protected Lab(bool enabled)
			: base(defaultTraceSourceName)
		{
			Contract.Ensures(!IsProxy);

			actualLab = this;

			IsEnabled = enabled;

#if NETFX_CORE
			var type = GetType().GetTypeInfo();
#else
			var type = GetType();
#endif

			displayName = Metadata.GetDisplayName(type, defaultLabName, suffixes);
			description = Metadata.GetDescription(type);
			categories = Metadata.GetCategories(type, suffixes);

#if !WINDOWS_PHONE
			listener = new Lazy<TraceListener>(CreateListener, System.Threading.LazyThreadSafetyMode.PublicationOnly);
#else
			listener = new Lazy<TraceListener>(CreateListener);
#endif

#if !WINDOWS_PHONE
			virtualExperiments = new Lazy<IList<IExperiment>>(() => MethodExperiment.FindAll(this), isThreadSafe: false);
#else
			virtualExperiments = new Lazy<IList<IExperiment>>(() => MethodExperiment.FindAll(this));
#endif
		}

		/// <summary>
		/// Constructs a new instance of <see cref="Lab"/> for derived classes.
		/// </summary>
		/// <param name="name">The name of the lab.</param>
		protected Lab(string name)
			: this(name, enabled: true)
		{
#if !WINDOWS_PHONE_70
			Contract.Requires(!string.IsNullOrWhiteSpace(name));
#else
			Contract.Requires(!string.IsNullOrEmpty(name));
#endif
			Contract.Ensures(!IsProxy);
		}

		/// <summary>
		/// Constructs a new instance of <see cref="Lab"/> for derived classes.
		/// </summary>
		/// <param name="name">The name of the lab.</param>
		/// <param name="enabled">Indicates whether the lab is enabled by default and assigns <see cref="IsEnabled"/>.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors",
				Justification = "Code Contracts rewriter")]
		protected Lab(string name, bool enabled)
			: base(name)
		{
#if !WINDOWS_PHONE_70
			Contract.Requires(!string.IsNullOrWhiteSpace(name));
#else
			Contract.Requires(!string.IsNullOrEmpty(name));
#endif
			Contract.Ensures(!IsProxy);

			this.displayName = name;

			actualLab = this;

			IsEnabled = enabled;

#if NETFX_CORE
			var type = GetType().GetTypeInfo();
#else
			var type = GetType();
#endif

			description = Metadata.GetDescription(type);
			categories = Metadata.GetCategories(type, suffixes);

#if !WINDOWS_PHONE
			listener = new Lazy<TraceListener>(CreateListener, System.Threading.LazyThreadSafetyMode.PublicationOnly);
#else
			listener = new Lazy<TraceListener>(CreateListener);
#endif

#if !WINDOWS_PHONE
			virtualExperiments = new Lazy<IList<IExperiment>>(() => MethodExperiment.FindAll(this), isThreadSafe: false);
#else
			virtualExperiments = new Lazy<IList<IExperiment>>(() => MethodExperiment.FindAll(this));
#endif
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors",
				Justification = "Code Contracts rewriter")]
		internal Lab(ILab actualLab)
			: base(defaultTraceSourceName)
		{
			Contract.Requires(actualLab != null);
			Contract.Ensures(IsProxy);
			Contract.Ensures(ActualLab == actualLab);

			this.actualLab = actualLab;

			Contract.Assert(ActualLab != this);

			IsEnabled = actualLab.IsEnabled;

#if NETFX_CORE
			var hostLab = actualLab as WindowsStoreConsoleLabHost;
#elif WINDOWS_PHONE
			var hostLab = actualLab as PhoneConsoleLabHost;
#elif SILVERLIGHT
			var hostLab = actualLab as SilverlightConsoleLabHost;
#else
			var hostLab = actualLab as WindowsConsoleLabHost;
#endif

			if (hostLab != null)
			{
				var hostedLab = hostLab.HostedLab;

				displayName = hostedLab.DisplayName;
				description = hostedLab.Description;

				if (hostedLab is AnonymousLab)
				{
					categories = new List<string>(0).AsReadOnly();
				}
				else
				{
					categories = hostedLab.Categories;
					Category = hostedLab.Category;
				}
			}
			else
			{
#if NETFX_CORE
				var type = actualLab.GetType().GetTypeInfo();
#else
				var type = actualLab.GetType();
#endif

				displayName = Metadata.GetDisplayName(type, defaultLabName, suffixes);
				description = Metadata.GetDescription(type);
				categories = Metadata.GetCategories(type, suffixes);
			}

#if !WINDOWS_PHONE
			listener = new Lazy<TraceListener>(CreateListener, System.Threading.LazyThreadSafetyMode.PublicationOnly);
#else
			listener = new Lazy<TraceListener>(CreateListener);
#endif

#if !WINDOWS_PHONE
			virtualExperiments = new Lazy<IList<IExperiment>>(() => MethodExperiment.FindAll(actualLab), isThreadSafe: false);
#else
			virtualExperiments = new Lazy<IList<IExperiment>>(() => MethodExperiment.FindAll(actualLab));
#endif
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
			Contract.Invariant(registeredTasks != null);
			Contract.Invariant(listener != null);
			Contract.Invariant(actualLab != null);
#if !WINDOWS_PHONE_70
			Contract.Invariant(!string.IsNullOrWhiteSpace(displayName));
#else
			Contract.Invariant(!string.IsNullOrEmpty(displayName));
#endif
			Contract.Invariant(description != null);
			Contract.Invariant(categories != null);
			Contract.Invariant(categories.IsReadOnly);
			Contract.Invariant(virtualExperiments != null);
			Contract.Invariant(!((ILab)this).HasProxy);
			Contract.Invariant(IsProxy || overriddenExperiments == null);
			Contract.Invariant(overriddenExperiments == null || overriddenExperiments.IsReadOnly);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]	// Avoids IsolatedStorageException: Unable to determine granted permission for assembly
		private static IsolatedStorageFile CreateStorage()
		{
#if SILVERLIGHT
			return IsolatedStorageFile.GetUserStoreForApplication();
#else
			return IsolatedStorageFile.GetUserStoreForAssembly();
#endif
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
				Justification = "This is a factory method.")]
		private TraceListener CreateListener()
		{
			Contract.Ensures(Contract.Result<TraceListener>() != null);

			return new AnonymousTraceListener(
					message => Trace(message),
					message => TraceLine(message),
					(cache, source, eventType, id, message) => TraceEvent(eventType, id, message));
		}

		/// <summary>
		/// Asynchronously ensures that execution continues on the UI thread.
		/// </summary>
		/// <returns></returns>
		public virtual Task EnsureUIThreadAccessAsync()
		{
			Contract.Ensures(Contract.Result<Task>() != null);

#if NETFX_CORE
			return WindowsStoreLab.EnsureUIThreadAccessAsync();
#elif WINDOWS_PHONE
			return PhoneLab.EnsureUIThreadAccessAsync();
#elif SILVERLIGHT
			return SilverlightLab.EnsureUIThreadAccessAsync();
#else
			return WindowsLab.EnsureUIThreadAccessAsync();
#endif
		}

		/// <summary>
		/// Formats the specified <paramref name="source"/> code for display.
		/// </summary>
		/// <param name="source">The source code to be formatted.</param>
		/// <returns>The formatted <paramref name="source"/> code.</returns>
		public virtual string FormatSourceCode(string source)
		{
			return source.Replace("\t", "    ");
		}

		/// <summary>
		/// Formats the specified <paramref name="source"/> XAML for display.
		/// </summary>
		/// <param name="source">The source XAML to be formatted.</param>
		/// <returns>The formatted <paramref name="source"/> XAML.</returns>
		public virtual string FormatSourceXaml(string source)
		{
			return source.Replace("\t", "    ");
		}

		/// <summary>
		/// Gets whether the <see cref="LabController"/> should call <see cref="Main"/>
		/// when the <see cref="Lab"/> is next in the queue to be executed.
		/// If <see cref="CanExecute"/> returns <see langword="false"/>, then the 
		/// <see cref="Lab"/> is skipped.
		/// </summary>
		/// <remarks>
		/// Derived classes can override <see cref="CanExecute"/> to include custom logic
		/// that determines whether the <see cref="Lab"/> should be skipped at runtime.
		/// </remarks>
		/// <returns>
		/// Returns <see langword="true"/> if <see cref="IsEnabled"/> is <see langword="true"/>
		/// and <see langword="false"/> otherwise.
		/// </returns>
		[Pure]
		public virtual bool CanExecute()
		{
			return IsEnabled;
		}

		/// <summary>
		/// Registers the specified <paramref name="task"/> as the main task.
		/// </summary>
		/// <param name="task">The task to be registered.</param>
		protected void RegisterMainTask(Task task)
		{
			Contract.Requires(task != null);

			registeredTasks.Enqueue(task);
		}

		/// <summary>
		/// Returns a <see cref="Task"/> that asynchronously completes when the <see cref="Main"/> method completes.
		/// </summary>
		/// <returns>A <see cref="Task"/> that asynchronously completes when the <see cref="Main"/> method completes</returns>
		protected internal Task WhenMainCompletesAsync()
		{
			Contract.Ensures(Contract.Result<Task>() != null);

			return registeredTasks.AwaitAllAsync();
		}

		internal async Task ExecuteAsync()
		{
			Contract.Requires(CanExecute());

			ResetTime();

			Contract.Assume(CanExecute());

			Main();

			await WhenMainCompletesAsync();
		}

		/// <summary>
		/// The entry point of the <see cref="Lab"/>, which is called automatically 
		/// by the <see cref="LabController"/> if <see cref="CanExecute"/> returns
		/// <see langword="true"/>.
		/// </summary>
		/// <remarks>
		/// <alert type="implement">
		/// When implemented by a derived class, <see cref="Main"/> must block the 
		/// current thread until the <see cref="Lab"/> has completed.  Asynchronous labs 
		/// are not currently supported.  By returning from <see cref="Main"/>, the 
		/// <see cref="Lab"/> indicates to the <see cref="LabController"/> that it has 
		/// completed and the next <see cref="Lab"/>, if any, can be started.
		/// </alert>
		/// </remarks>
		protected abstract void Main();

		internal IList<IExperiment> OverrideExperiments(IEnumerable<IExperiment> newExperiments)
		{
			Contract.Requires(newExperiments != null);
			Contract.Requires(IsProxy);
			Contract.Ensures(OverriddenExperiments != null);
			Contract.Ensures(Contract.Result<IList<IExperiment>>() != null);

			overriddenExperiments = newExperiments.ToList().AsReadOnly();

			Contract.Assume(overriddenExperiments.IsReadOnly);

			return overriddenExperiments;
		}

		/// <summary>
		/// Sequentially executes the lab's experiments asynchronously.
		/// </summary>
		protected internal async void RunExperiments()
		{
			await RunExperimentsAsync().ConfigureAwait(false);
		}

		/// <summary>
		/// Sequentially executes the lab's experiments asynchronously.
		/// </summary>
		/// <returns></returns>
		protected internal Task RunExperimentsAsync()
		{
			Contract.Ensures(Contract.Result<Task>() != null);

			var task = RunExperimentsAsync(ExperimentStartingAsync, ExperimentExecutingAsync, ExperimentCompletedAsync);

			RegisterMainTask(task);

			return task;
		}

		internal Task RunExperimentsAsync(IGUIConsoleLab lab)
		{
			Contract.Requires(lab != null);
			Contract.Ensures(Contract.Result<Task>() != null);

			var task = RunExperimentsAsync(lab.ExperimentStartingAsync, lab.ExperimentExecutingAsync, lab.ExperimentCompletedAsync);

			RegisterMainTask(task);

			return task;
		}

		private async Task RunExperimentsAsync(
				Func<IList<IExperiment>, int, Task<bool>> starting,
				Func<IList<IExperiment>, int, Task> executing,
				Func<IList<IExperiment>, int, Task> completed)
		{
			Contract.Requires(starting != null);
			Contract.Requires(executing != null);
			Contract.Requires(completed != null);

			IsRunningAllExperiments = true;

			try
			{
				var currentExperiments = OverriddenExperiments;

				var list = currentExperiments as IList<IExperiment> ?? currentExperiments.ToList();

				if (!list.IsReadOnly)
				{
					list = new System.Collections.ObjectModel.ReadOnlyCollection<IExperiment>(list);
				}

				for (int i = 0; i < list.Count; i++)
				{
					Contract.Assume(list.IsReadOnly);

					if (!(await ExecuteExperimentAsync(list, i, starting, executing, completed)))
					{
						break;
					}
				}
			}
			finally
			{
				IsRunningAllExperiments = false;
			}

			Contract.Assert(IsProxy || overriddenExperiments == null);
			Contract.Assert(overriddenExperiments == null || overriddenExperiments.IsReadOnly);
		}

		internal Task<bool> ExecuteExperimentAsync(IGUIConsoleLab lab, IExperiment experiment)
		{
			Contract.Requires(lab != null);
			Contract.Requires(experiment != null);

			IList<IExperiment> experiments = new List<IExperiment>(1)
				{
					experiment 
				}
					.AsReadOnly();

			Contract.Assume(experiments.Count == 1);
			Contract.Assume(experiments.IsReadOnly);

			return ExecuteExperimentAsync(experiments, 0, lab.ExperimentStartingAsync, lab.ExperimentExecutingAsync, lab.ExperimentCompletedAsync);
		}

		private async Task<bool> ExecuteExperimentAsync(
				IList<IExperiment> experiments,
				int index,
				Func<IList<IExperiment>, int, Task<bool>> starting,
				Func<IList<IExperiment>, int, Task> executing,
				Func<IList<IExperiment>, int, Task> completed)
		{
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Requires(starting != null);
			Contract.Requires(executing != null);
			Contract.Requires(completed != null);

			ResetTime();

			var running = new TaskCompletionSource<bool>();

			RegisterMainTask(running.Task);

			try
			{
				await EnsureUIThreadAccessAsync().ConfigureAwait(false);

				if (!(await starting(experiments, index).ConfigureAwait(false)))
				{
					return false;
				}

				await EnsureUIThreadAccessAsync().ConfigureAwait(false);

				await executing(experiments, index).ConfigureAwait(false);

				await EnsureUIThreadAccessAsync().ConfigureAwait(false);

				await completed(experiments, index).ConfigureAwait(false);
			}
			catch (Exception ex)
			{
				running.SetException(ex);
				throw;
			}

			running.SetResult(true);

			return true;
		}

		/// <summary>
		/// Traces information about the next experiment to be executed from the specified list of <paramref name="experiments"/>.
		/// </summary>
		/// <param name="experiments">The list of experiments from which the next experiment will be executed.</param>
		/// <param name="index">The index of the experiment in the <paramref name="experiments"/> list that is the next to be executed.</param>
		/// <param name="additionalInfo">Any additional information to be traced.</param>
		protected internal virtual void TraceExperimentHeader(IList<IExperiment> experiments, int index, string additionalInfo)
		{
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			var experiment = experiments[index];

			Contract.Assume(experiment != null);

			if (experiments.Count == 1)
			{
				TraceStatus(Format(Resources.ExperimentTitleFormat, experiment.DisplayName, additionalInfo));
			}
			else
			{
				Contract.Assume(index < int.MaxValue);

				TraceStatus(Format(Resources.ExperimentInfoFormat, experiment.DisplayName, index + 1, experiments.Count, additionalInfo));
			}
		}

		/// <summary>
		/// Called before an experiment begins executing and asynchronously returns a value indicating whether the experiment may be executed or skipped.
		/// </summary>
		/// <param name="experiments">The list of experiments from which the next experiment will be executed.</param>
		/// <param name="index">The index of the experiment in the <paramref name="experiments"/> list that is the next to be executed.</param>
		/// <returns>A <see cref="Task{T}"/> that eventually completes with a value of <see langword="true"/> to indicate that the experiment may be executed; otherwise, <see langword="false"/> to indicate that the experiment must be skipped.</returns>
		protected internal virtual Task<bool> ExperimentStartingAsync(IList<IExperiment> experiments, int index)
		{
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			TraceExperimentHeader(experiments, index, null);

#if (SILVERLIGHT && !WINDOWS_PHONE) || NET40
			return TaskEx.FromResult(true);
#else
			return Task.FromResult(true);
#endif
		}

		/// <summary>
		/// Asynchronously executes the experiment at the specified <paramref name="index"/>.
		/// </summary>
		/// <param name="experiments">The list of experiments from which the next experiment will be executed.</param>
		/// <param name="index">The index of the experiment in the <paramref name="experiments"/> list that is the next to be executed.</param>
		/// <returns>A <see cref="Task"/> that represents the asynchronous state of the experiment being executed.</returns>
		protected internal virtual async Task ExperimentExecutingAsync(IList<IExperiment> experiments, int index)
		{
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			var experiment = experiments[index];

			Contract.Assume(experiment != null);

			TraceLine();

			await experiment.ExecuteAsync();

			TraceLine();
		}

		/// <summary>
		/// Called after an experiment has executed.
		/// </summary>
		/// <param name="experiments">The list of experiments from which the experiment at the specified <paramref name="index"/> was executed.</param>
		/// <param name="index">The index of the experiment in the <paramref name="experiments"/> list that was executed.</param>
		/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
		protected internal virtual Task ExperimentCompletedAsync(IList<IExperiment> experiments, int index)
		{
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			TraceExperimentHeader(experiments, index, Resources.Completed_Inline);

#if (SILVERLIGHT && !WINDOWS_PHONE) || NET40
			return TaskEx.FromResult(true);
#else
			return Task.FromResult(true);
#endif
		}
		#endregion
	}

	[ContractClassFor(typeof(Lab))]
	internal abstract class LabContract : Lab
	{
		protected override void Main()
		{
			Contract.Requires(CanExecute());
		}
	}
}